Skip to content

XL-0-SERVICE (parked): reproduce the shell service-emission fabrication — stdout bound to every declared output field, unboxed error arm - #9867

Merged
gunbai-bot[bot] merged 2 commits into
mainfrom
session/calm-boar-314
Sep 1, 2026
Merged

gunbai-bot[bot] merged 2 commits into
mainfrom
session/calm-boar-314

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

PARKED by operator directive (typeck-to-zero is the only funded priority). This PR carries the REPRODUCTION ONLY, not the repair.

  • Established: the defect is reproduced by execution from a declaration, its binding rule is identified and is not what the handoff assumed, and its blast radius is measured (latent, but 209 declarations armed). The channel-vocabulary census the repair needs is done and is written below.
  • Remains: the repair itself — one shared output-field→source projection that refuses when it cannot bind, a closed shell-channel coproduct replacing two fabricating fallback arms, and one shared error-return authority replacing the REST/shell fork.
  • Single next action: implement the repair in src/v1/05_emit_rust.dag (emit_shell_return, emit_shell_channel_expr, emit_exit_code_handling, emit_exit_arm) plus the shared vocabulary in v1.compiler.emit, regenerate src/v1/stage0/src/v1_compiler_emit_rust.rs from the merged authority (never merge-resolve the mirror), and flip the probe in this PR to assert the refusal.

The defect

src/v1/05_emit_rust.dag's shell service-emission path emits Rust that does not compile, with zero diagnostics. Reproduced by execution — compile_sources on an inline module, RenderTarget::Rust, run remotely on BuildBuddy.

Declaration:

service Probe {
  operation Version {
    input {}
    output {
      success: Bool from "exit_success"
      out: String from "stdout"
      err: String from "stderr"
    }
    transport shell { argv: ["git", "--version"] }
  }
}

Emitted, verbatim:

pub async fn version(&self) -> Result<(bool, String, String), Box<dyn std::error::Error>> {
    ...
    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
    Ok((stdout.clone(), stdout.clone(), stdout.clone()))
}

Two type errors in four lines. This is DESIGN §5's fabricated-plausible-output arm applied to the emitter: a producer that cannot answer a question answers anyway.

Refuted hypothesis — the binding rule is none of the three candidates

The handoff proposed the rule was "always the first output", "always the last", or "always the one named stdout". It is none of them. It is stdout for every field, unconditionally, because the declared source never reaches the renderer: child_from_key returns Absent for all three fields, and each then falls through Absent => "stdout" in emit_shell_return and the trailing else { stdout.clone() } in emit_shell_channel_expr.

The three-output fixture is what made this decidable, and the evidence is an absence. With two fields, "always the first output" and "always stdout" emit identical bytes — the originally observed case could not separate them. With three, they diverge. And the decisive datum is the missing let stderr = String::from_utf8_lossy(&output.stderr).to_string(); prelude line: the renderer emits that line only when some field claims the stderr channel, so its absence proves the from "stderr" field was invisible to the renderer, not merely mis-ordered.

So there are two defects stacked, and neither alone is the repair:

  1. the declared from key is lost somewhere between parse (parse_optional_from_key mints it as a from_key property) and the shell renderer — note the interpreter's map_shell_outputs reads the same property off the same children through extract_from_key and works, so the loss is on the emit path;
  2. when a field's source is unresolvable the renderer fabricates stdout instead of refusing, which is exactly what made (1) silent for as long as it has existed.

Fixing only (2) turns 209 working-looking declarations into refusals. Fixing only (1) leaves the fabricating arm armed for the next fact that goes missing.

Blast radius — latent, but armed

Verified three ways, not assumed:

  • No emitted service surface is committed anywhere in the tree. DryRunMode, dry_run.is_dry_run() and output.status.code().unwrap_or(-1) each occur in exactly one file, src/v1/stage0/src/v1_compiler_emit_rust.rs, and there only inside the emitter's own template strings.
  • Zero service declarations exist under src/v1, which is the closure regen seeds into stage0. This path has therefore never produced a committed mirror.
  • The latent population is large: 296 shell-transport operations across the corpus, of which 209 declare from "exit_success" (i.e. are multi-field). Every one emits non-compiling Rust the moment its module enters an emitted closure — which is a thing v2 migration does routinely. Latent is not safe; it is armed.

Channel-vocabulary census (needed before the coproduct can be closed)

The repair replaces a trailing else with an exhaustive match, so every from "…" string in the corpus must be a declared member or it refuses. Enumerated first rather than discovered one refusal at a time. Re-derive with:

# walks every .dag under dag/ src/v1 src/v2 fixtures/ test/, brace-tracks each
# `operation X { … }`, reads its `transport` kind and its `output { … }` block,
# and tallies the distinct `from "…"` keys per transport kind.

(brace-tracking extractor; 386 operations found — 296 shell, 76 rest, 6 file, 8 with no transport)

Distinct shell from-keys — 8, and only 4 are realized by the Rust emitter today:

key occurrences Rust emitter interpreter
stdout 221 realized realized
exit_success 209 realized realized
stderr 172 realized realized
exit_code 107 fabricates stdout realized
stdout_lines 8 realized realized
stderr_truncated 1 fabricates stdout realized
stderr_total_bytes 1 fabricates stdout realized
stderr_retained_bytes 1 fabricates stdout realized

Two consequences the repair must carry:

  • exit_code is 107 declarations silently emitting stdout.clone() for an integer channel. This is the largest single instance of the fabrication and was invisible because the else arm answered for it.
  • Zero shell output fields lack a from key (checked: 0 of 296). So closing the coproduct refuses nothing that exists today, provided all 8 members are modeled. No typos or invented channels were found — the wall exposes no pre-existing authoring errors, only the emitter's own unrealized channels.
  • The interpreter (v1_interpreter::shell_evidence_value + map_shell_outputs) already models 13 channels including byte-accounting ones (*_total_bytes, *_retained_bytes, *_truncated, *_digest_hex). That is a §3 fork: one fact — which shell channels exist — implemented twice, unequally. The byte-accounting channels are genuinely not realizable by Command::output() (they only differ under the interpreter's bounded drain), so the honest emitter arm for them is a typed located refusal, not a value.

The error arm is a §3 fork, not a missing .into()

emit_exit_code_handling and emit_exit_arm both emit Err(<String expr>) against a declared Box<dyn std::error::Error>. The REST arm ~200 lines above, in the same emitter, writes Err(format!(…).into()). One fact — how this emitter returns an error — is implemented twice and the two implementations disagree. The repair is one shared error-return authority read by both arms, not a second .into(); otherwise the fork stands with both halves correct today and free to diverge again.

What is in this PR

One #[test] in src/v1/stage0/src/compiler_tests.rs: a §4b(4) known-hole probe, green today, asserting the wrong behavior (Ok((stdout.clone(), stdout.clone(), stdout.clone())), and the absent stderr prelude). When the wall lands it must flip to the refusal assertion and stay enrolled as a permanent regression control. It is not a passing check of correct behavior and must not be read as one.

Sequencing note for the resumer

Three lanes were editing 05_emit_rust.dag concurrently. #9850 (bold-carp-449) is in main and merged here. #9854 (stern-ferret-752) and #9862 (bold-carp-449, a shared predicate that renames symbols in this file) were both still open at park time — check them before touching the authority. The mirror v1_compiler_emit_rust.rs must never be merge-resolved: whoever integrates second regenerates it from the merged .dag authority.

…h a three-output known-hole probe

The shell service-emission path in 05_emit_rust binds `stdout` to EVERY declared
output field, whatever source the declaration named, and returns a bare String
from the error arm against a declared Box<dyn std::error::Error>. Both emit Rust
that does not compile, with zero diagnostics.

This commit lands the reproduction only -- the lane was parked by the operator
before the repair (it does not reduce the typeck count). The probe is a §4b(4)
known-hole probe: green today asserting the WRONG behavior, and it must flip to
the refusal assertion when the wall lands.

The three-output fixture is the load-bearing part. Two fields cannot distinguish
"wrong tuple index" from "the declared source never arrived"; three can, and the
ABSENCE of the emitted `let stderr = ...` prelude is the positive evidence that
the `from "stderr"` field was invisible rather than mis-ordered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKidppJZFSPywEdbVGJ1Ex
@gunbai-bot
gunbai-bot Bot marked this pull request as draft September 1, 2026 02:04
@gunbai-bot

gunbai-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Loss site pinned: the from / from_key nickname (§3), not a lost property

Instrumented run (temporary eprintln! inside child_from_key in the generated mirror, reverted before commit; BuildBuddy invocation, three-output fixture from the probe in this PR). Every declared output child reaches the shell renderer carrying its property:

PROBE child_from_key: name="success" props=["from"]
PROBE child_from_key: name="out"     props=["from"]
PROBE child_from_key: name="err"     props=["from"]

The property is not lost. It is present on all three children — and its resolved name is "from", while v1.compiler.emit's child_from_key filters on == "from_key" and therefore matches nothing, on every field, always.

This corrects the diagnosis in the body above (which said the key was "lost between parse and the shell renderer"). It is not lost; it is nicknamed. parse_optional_from_key mints the property with the synthetic name "from_key", but the name a reader gets back through field_init_node_name_at is resolved from the authored source span — the token the author actually wrote, which is from. One property, two names.

This is exactly why the interpreter works and the emitter does not. v1_interpreter::extract_from_key reads:

if prop_name == "from_key" || prop_name == "from" {

— it accepts both spellings. child_from_key accepts only one. Two readers of one fact, disagreeing about the fact's name: DESIGN §3's nickname failure, with the two halves having silently diverged. The interpreter's || prop_name == "from" is the tell that someone met this before and repaired it on their side only.

What this changes for the repair

It makes the root cheaper and the walls more necessary, not less:

  • The name must have one authority. Reading the property through a single shared accessor (which child_from_key was already supposed to be — 05_emit.dag says it was moved there so the wall and the renderer "read the operation's output contract through one reader rather than two that happen to agree") and deleting the second spelling from the interpreter. Adding || "from" to child_from_key would fix the symptom by cementing the nickname — two names for one property, now agreed on by three readers instead of two. That is the wrong repair.
  • The fabricating arms still land as specified. This defect was silent for its entire life because Absent => "stdout" answered for it. A reader that cannot name the property it is looking for is precisely the case §5 says must refuse. Had the fallthrough refused, this nickname would have been a loud diagnostic on the first service declaration instead of a handoff.
  • The exit_code finding (107 declarations, integer channel, silently emitting stdout.clone()) is independent of this and survives the name fix — it is the trailing else in emit_shell_channel_expr, a second fabricating arm reached through a correctly-read key.

So the repair is three things, and the ordering matters: fix the name (one authority), close the channel coproduct (exhaustive, 8 measured members, refusing the 4 the Rust emitter cannot honestly realize), and replace both fabricating fallbacks and the REST/shell error-arm fork with refusals and one shared error-return authority.

@briansrls
briansrls marked this pull request as ready for review September 1, 2026 03:01
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-01T03:04:05.612590Z 90bd2fe Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90bd2fefe9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/v1/stage0/src/compiler_tests.rs Outdated
Comment on lines +3151 to +3152
#[test]
fn shell_service_output_projection_fabricates_stdout_known_hole_probe() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the probe to the compiler-tests authority

This edits only the generated compiler_tests.rs mirror, while emit_compiler_tests_module recreates that path from v1.compiler.compiler_tests_rust::compiler_tests_source(), whose producer list contains no version of this probe. Consequently the required-regen comparison will report drift, and any regeneration will delete the test. Define and enroll the probe in src/v1/compiler_tests_rust.dag, then regenerate this mirror.

Useful? React with 👍 / 👎.

Comment thread src/v1/stage0/src/compiler_tests.rs Outdated
Comment on lines +3151 to +3152
#[test]
fn shell_service_output_projection_fabricates_stdout_known_hole_probe() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enroll the probe in the retained test crate

Checked src/v2/workflow/ci_v1_compiler_tests_compile_gate_emit.dag: the CI execution gate runs cargo test -p v1-compiler-tests --release, but this test belongs to the dependency crate's #[cfg(test)] mod compiler_tests. Cargo does not enable a dependency's unit-test configuration while testing the separate v1-compiler-tests package, so this probe is never executed by that gate and provides no signal when the defect changes. Put the probe in the retained package under src/v1/tests or explicitly enroll the v1-compiler lib-test target.

Useful? React with 👍 / 👎.

Comment thread src/v1/stage0/src/compiler_tests.rs Outdated
Comment on lines +3191 to +3192
assert!(
emitted.contains("Ok((stdout.clone(), stdout.clone(), stdout.clone()))"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert the unboxed error arm in the probe

The probe and its surrounding description claim to reproduce both stdout fabrication and the bare-String error arm, but the assertions inspect only the success tuple and missing stderr prelude. If the error arm changes independently, this test remains green and supplies no evidence that the second reported defect is present or repaired. Add a discriminating assertion for the emitted Err(...) expression, including the currently missing boxing conversion.

Useful? React with 👍 / 👎.

…egenerate the stage0 mirror

The parked commit hand-wrote the probe into src/v1/stage0/src/compiler_tests.rs,
which is a GENERATED file -- the emitted-population manifest names it, so the next
regeneration would have silently deleted it. The authority for that surface is
src/v1/compiler_tests_rust.dag.

This commit authors ct_shell_service_output_projection_known_hole_probe_test()
there, enrolls it in compiler_tests_source(), and carries both generated
projections that follow from it:

  - src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs (the mirror of the
    generator module itself), and
  - src/v1/stage0/src/compiler_tests.rs (the generator's own output), where the
    probe now sits at the position compiler_tests_source() places it rather than
    at end-of-file.

Both were taken from --required-regen candidate bytes, not hand-edited: round one
reported exactly one generated-surface drift (v1_compiler_compiler_tests_rust.rs),
and round two -- with the seed rebuilt from that mirror -- reported exactly one
more (compiler_tests.rs). The probe's own bytes are unchanged; it still asserts
today's WRONG behavior and must flip when the wall lands.
@gunbai-bot
gunbai-bot Bot merged commit 6741a39 into main Sep 1, 2026
6 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/calm-boar-314 branch September 1, 2026 04:13
@briansrls
briansrls restored the session/calm-boar-314 branch September 1, 2026 04:15
briansrls pushed a commit that referenced this pull request Sep 1, 2026
#9867 and this branch each added a witness to compiler_tests_rust.dag. The
.dag authority merged cleanly and carries both; only the two generated
projections conflicted, and the driver refused them with no conflict markers
and ours-side bytes in the worktree.

Regenerated from the merged authority rather than picking a side. The
candidate carries both ct_import_lines_follow_resolved_binding_identity_test
and ct_shell_service_output_projection_known_hole_probe_test -- taking either
side would have dropped the other silently.

Two rounds were needed because v1_compiler_compiler_tests_rust.rs is the
emitter and compiler_tests.rs is what it emits, so each install reveals the
next layer on rebuild. first_generation_equal=true at round 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDnHQs9oE5AKJXe1vReU8v
@gunbai-bot

gunbai-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Census: where the Rust emitter guesses a callee's INSTANTIATED parameter type

Requested by gentle-koi-869 after their target_carriers.rs:105 and v2_compiler_compile.rs:288
repairs both bottomed out in one missing capability: nothing records the per-call type-argument
substitution, so the emitter reads the DECLARED parameter type (or the enclosing expression's
type) wherever it needs the instantiated one.

Censused from the EMITTER SOURCE, at symbol grain, not from the rustc error list -- the error list
is selected on "the guess was wrong AND rustc noticed", which is the safe half.

The six decision sites (all in v1.compiler.emit_rust unless noted)

  1. rust_call_arg_fail_closed_unwrap
    Reads: param_node_type_expr of the callee's declared param -> cardinality / Optional carrier.
    Decides: whether to inject .expect("fail-closed: ...") on the argument.
    Unanswerable when the declared type is the callee's own type variable: optionality is a property
    of the INSTANTIATION. Both arms are guesses -- T may instantiate to Optional<X> (unwrap
    wrong) or to X (unwrap right). gentle-koi's repair makes it decline; a decline is a declared
    inability, not an answer.
    FAILS LOUD when wrong (E0308), which is why this one was found.

  2. rust_call_arg_function_value_adapt
    Reads: param_node_type_expr(n: param).params |> count -- the declared ARITY of a
    function-typed parameter.
    Decides: whether to wrap the argument in the __adapt_f forwarding closure that closes the
    Rc<dyn Fn> -> impl Fn seam.
    A parameter declared as a bare type variable has arity 0, so the adapter is silently NOT emitted;
    a declared arrow whose instantiation differs is the same guess.
    FAILS SILENT in the negative direction -- no adapter, no diagnostic, and the E0277 only appears
    if the seam happens to be crossed. This is the dangerous half.

  3. rust_witness_type_arg_from_expected_type (and its caller rust_witness_variant_ctor_path)
    Reads: emit_info.expected_type.
    THE SYSTEMIC FACT behind gentle-koi's :288: emit_typed_call emits every argument with
    emit_info passed through UNCHANGED, and the ONLY writer of expected_type in the whole module
    is the record-field path (emit_field_value_with_context, two call sites). So at an argument
    position expected_type is BY CONSTRUCTION never the argument's parameter type -- it is
    whichever enclosing record field, or none. This is not a bug at the reader; the writer for the
    argument position does not exist.

  4. record_field_expected_type / rust_struct_field_type_node_from_container
    The same missing substitution one grain over: a generic struct's DECLARED field type is used as
    the expected type of the field value, with the literal's type arguments never applied.
    Included because the capability that fixes 1-3 is the same one that fixes this, and excluding it
    would ship the capability at call grain only.

  5. v1_call_forwarding_forwarded_param_names
    Already performs a substitution -- ad hoc, by STRING equality: it compares the callee's declared
    param type name and its type-argument names against the argument's resolved type names to decide
    trait-bound forwarding. Half the capability exists here, hand-rolled and unshared. When the real
    substitution lands, this is a consumer, not a survivor (DESIGN section 3: two structures
    answering one semantic question).

  6. fill_default_args / fill_op_default_args
    Splices a default expression declared in the CALLEE into the caller's argument list, emitted in
    the caller's scope with no substitution. No measured instance; listed to close the census.

NOT a member, named so the census is closed: emit_typed_method_call's service-op path reads
declared op_params optionality and JSON coercion. Service operations carry no type parameters, so
that read is answerable by construction.

Population

Import closure of v2.compiler.compile over dag/ + src/v2/: 158 modules.

call sites to generic callees                                          3507
  >=1 VALUE param declared as a BARE type variable (T / T?)            1258   (19 callees)
  >=1 value param's declared type MENTIONS a type variable             2566   (78 callees)
  >=1 value param is function-typed (the arity site 2 reads)            989

The 1258 is site 1's exact unanswerable condition. The largest contributors:

 505  outcome_accepted<T>(value: T)          <- gentle-koi's :105 is one of these
 247  optional_present<T>(value: T)
 174  fold_list<T, A>(empty: A)
 153  list_snoc_item<T>(item: T)
  69  map_insert<K, V>(key: K, value: V)
  44  outcome_with_diagnostics<U>(value: U)

Two anecdotes were 2 of 1258.

What these numbers DO and DO NOT establish

DO: the emitter cannot answer at 1258 call sites in the closure. That is a property of the
declarations and is exact up to the approximation below.

DO NOT: how many of the 1258 the sites actually FIRE on. Site 1 injects only when the argument is
additionally optional-typed, which is a resolved-type fact no textual pass can read. Measured
instead on the committed emitted bytes: the .expect("fail-closed: ...") string occurs 9 times in
src/v1/stage0/, and EVERY ONE is on a monomorphic callee (semver_compare_identifier,
occurrence_id_eq, schedule_witness_entry_eq, runnable_eq, runnable_batch_eq,
oci_wire_digest_parts_from_split) -- declared types concrete, read answerable, injection legitimate.
Zero fire on a generic callee in committed bytes. That is exactly why the class stayed invisible
until 00_compile was emitted: the unanswerable population lives in emitted output that is not
committed.

APPROXIMATION, stated because it bounds the number: the closure is the TEXTUAL import closure and
call sites are counted by name-with-open-paren over comment-stripped source, so a same-named
monomorphic function inflates and a call reached only through a value-position reference deflates.
It is a sizing instrument, not a gate.

Re-derivation

Two throwaway scripts, run from the repo root; they are pasted here rather than committed because
they are a one-off sizing pass, not a maintained artifact. The DURABLE instrument for this question
is a src/v2/lens/ reader over the same Node tree -- a call site whose callee parameter type is a
bare type variable is a pure structural read -- and I did not build it. If this capability lands
with a gate, that lens is what the gate should run, and the numbers above should be re-derived by
it rather than trusted from this document.

closure.py
import os,re,sys,json
roots=['dag','src/v2']
mod2file={}
for r in roots:
    for dp,_,fs in os.walk(r):
        for f in fs:
            if f.endswith('.dag'):
                p=os.path.join(dp,f)
                txt=open(p,encoding='utf8',errors='replace').read()
                m=re.search(r'^module\s+([\w.]+)',txt,re.M)
                if m: mod2file[m.group(1)]=p
start=None
for m,p in mod2file.items():
    if p=='src/v2/compiler/00_compile.dag': start=m
seen=set();stack=[start]
while stack:
    m=stack.pop()
    if m in seen or m not in mod2file: continue
    seen.add(m)
    txt=open(mod2file[m],encoding='utf8',errors='replace').read()
    for im in re.findall(r'^import\s+([\w.]+)',txt,re.M):
        if im not in seen: stack.append(im)
print(json.dumps({'start':start,'count':len(seen),'files':sorted(mod2file[m] for m in seen if m in mod2file)}))
census2.py (run: python3 census2.py, expects closure.py beside it)
import os,re,json,subprocess
files=json.loads(subprocess.check_output(['python3',os.path.dirname(__file__)+'/closure.py']).decode())['files']
def sigs(txt):
    out=[]
    for m in re.finditer(r'\bfn\s+(\w+)<([^>]*)>\s*\(',txt):
        i=m.end()-1;d=0
        for j in range(i,len(txt)):
            if txt[j]=='(':d+=1
            elif txt[j]==')':
                d-=1
                if d==0: break
        out.append((m.group(1),[t.strip() for t in m.group(2).split(',')],txt[i+1:j]))
    return out
gen={}
for p in files:
    txt="\n".join(l for l in open(p,encoding="utf8",errors="replace") if not l.lstrip().startswith("//"))
    for name,tps,params in sigs(txt):
        ps=[]
        d=0;cur=''
        for ch in params:
            if ch in '<([': d+=1
            elif ch in '>)]': d-=1
            if ch==',' and d==0: ps.append(cur);cur=''
            else: cur+=ch
        if cur.strip(): ps.append(cur)
        pl=[]
        for x in ps:
            if ':' in x:
                nm,ty=x.split(':',1); pl.append((nm.strip(),ty.split('=')[0].strip()))
        bare=[ (n,t) for n,t in pl if t.rstrip('?') in tps ]
        ment=[ (n,t) for n,t in pl if any(re.search(r'(?<![\w])'+re.escape(tp)+r'(?![\w])',t) for tp in tps) ]
        gen[name]=dict(file=p,tps=tps,params=pl,bare=bare,mentions=ment)
calls={}
for p in files:
    txt="\n".join(l for l in open(p,encoding="utf8",errors="replace") if not l.lstrip().startswith("//"))
    for name in gen:
        n=len(re.findall(r'(?<![\w.])'+name+r'\s*\(',txt))-len(re.findall(r'fn\s+'+name+r'\s*<',txt))
        if n>0: calls[name]=calls.get(name,0)+n
tot=sum(calls.values())
bare_calls=sum(v for k,v in calls.items() if gen[k]['bare'])
ment_calls=sum(v for k,v in calls.items() if gen[k]['mentions'])
fnty=sum(v for k,v in calls.items() if any('fn(' in t.replace(' ','') for _,t in gen[k]['params']))
print(f"call sites to generic callees: {tot}")
print(f"  ... where >=1 VALUE param is declared as a BARE type variable (T / T?): {bare_calls}  ({sum(1 for k in calls if gen[k]['bare'])} distinct callees)")
print(f"  ... where >=1 value param's declared type MENTIONS a type variable:     {ment_calls}  ({sum(1 for k in calls if gen[k]['mentions'])} distinct callees)")
print(f"  ... where >=1 value param is function-typed (arity read by adapt):      {fnty}")
print()
for k,v in sorted(calls.items(),key=lambda x:-x[1]):
    if gen[k]['bare'] and v>=10:
        print(f"{v:5d}  {k}<{','.join(gen[k]['tps'])}>  bare-tv params: {gen[k]['bare']}")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants